/*
 * Copyright (C) 2011 Emmanuel Tourdot
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 * $Id$
 */
package org.etourdot.xinclude.stax;

import com.google.common.base.Strings;
import com.google.common.io.Closeables;
import org.etourdot.xinclude.XIncProcEngine;
import org.etourdot.xinclude.XIncProcEngineHandler;
import org.etourdot.xinclude.XIncludeException;
import org.etourdot.xinclude.xpointer.XPointer;
import org.etourdot.xinclude.xpointer.XPointerContext;
import org.etourdot.xinclude.xpointer.XPointerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.util.Stack;

/**
 * @author Emmanuel Tourdot
 */
public class XIncludeStaxEngine implements XIncProcEngineHandler
{
    private static final Logger LOG = LoggerFactory.getLogger(XIncludeStaxEngine.class);
    private final Stack<URI> baseURIStack = new Stack<URI>();
    private URI baseURI;
    private final XIncProcStaxFactory factory;

    public XIncludeStaxEngine(XIncProcStaxFactory factory)
    {
        this.factory = factory;
    }

    @Override
    public void parse(final URI baseURI, final OutputStream output) throws XIncludeException
    {
        final Source inputSource = new StreamSource(new File(baseURI.getRawPath()));
        final Result outputResult = new StreamResult(output);
        parse(inputSource, outputResult);
    }

    @Override
    public void parse(final Source source, final Result result) throws XIncludeException
    {
        try
        {
            this.baseURI = new URI(source.getSystemId());
            baseURIStack.push(baseURI);
            final XMLEventReader reader = factory.getXmlInputFactory().createXMLEventReader(source);
            final XMLEventWriter writer = factory.getXmlOutputFactory().createXMLEventWriter(result);
            processFile(reader, writer, false);
            writer.flush();
            writer.close();
        }
        catch (final URISyntaxException e)
        {
            throw new XIncludeException(e);
        }
        catch (final XMLStreamException e)
        {
            throw new XIncludeException(e);
        }
    }

    @Override
    public void parse(final InputStream source, final String systemId, final OutputStream result) throws XIncludeException
    {
        try
        {
            this.baseURI = new URI(systemId);
            baseURIStack.push(baseURI);
            final XMLEventReader reader = factory.getXmlInputFactory().createXMLEventReader(source);
            final XMLEventWriter writer = factory.getXmlOutputFactory().createXMLEventWriter(result);
            processFile(reader, writer, false);
            writer.flush();
            writer.close();
        }
        catch (final URISyntaxException e)
        {
            throw new XIncludeException(e);
        }
        catch (final XMLStreamException e)
        {
            throw new XIncludeException(e);
        }
    }

    private void processFile(final XMLEventReader reader, final XMLEventWriter writer, final boolean inFallback)
                                throws XMLStreamException, XIncludeException
    {
        int level = 0;
        boolean endReadched = false;
        while (reader.hasNext() && !endReadched)
        {
            final XMLEvent event = reader.nextEvent();
            switch (event.getEventType()) {
                case XMLStreamConstants.START_DOCUMENT:
                    break;
                case XMLStreamConstants.END_DOCUMENT:
                    break;
                case XMLStreamConstants.DTD:
                    break;
                case XMLStreamConstants.START_ELEMENT:
                    final StartElement startElement = event.asStartElement();
                    if (XIncProcEngine.isXInclude(startElement.getName()))
                    {
                        processXInclude(startElement, reader, writer);
                    }
                    else
                    {
                        if (XIncProcEngine.isFallback(startElement.getName()))
                        {
                            throw new XIncludeException("Fallback not in xinclude element container");
                        }
                        level ++;
                        writer.add(startElement);
                        if (level == 1 && !inFallback) {
                            writeBaseAttribute(writer, null);
                        }
                    }
                    break;
                case XMLStreamConstants.END_ELEMENT:
                    if (XIncProcEngine.isFallback(event.asEndElement().getName()) && inFallback)
                    {
                        endReadched = true;
                    }
                    else
                    {
                        level --;
                        writer.add(event);
                    }
                    break;
                default:
                    writer.add(event);
                    break;
            }
        }
    }

    private void processXInclude(final StartElement startElement, final XMLEventReader reader,
                                 final XMLEventWriter writer) throws XIncludeException, XMLStreamException
    {
        try
        {
            // Verifying attributes
            validateAttributes(startElement);
            final Attribute parseAtt = startElement.getAttributeByName(new QName("parse"));
            final String parse = (parseAtt==null)? "xml" : parseAtt.getValue();
            final Attribute xpointerAtt = startElement.getAttributeByName(new QName("xpointer"));
            final String xpointer = (xpointerAtt==null)? null : xpointerAtt.getValue();
            if ("xml".equals(parse))
            {
                // Processing xml document
                includeXmlContent(startElement, writer, xpointer);
            }
            else
            {
                // Processing text document
                includeTextContent(startElement, writer);
            }
        }
        catch (final XIncludeException xie)
        {
            processFallback(reader, writer);
        }
        // Skip until xinclude end element
        boolean endReadched = false;
        while (reader.hasNext() && !endReadched)
        {
            final XMLEvent ev = reader.nextEvent();
            if (ev.getEventType() == XMLStreamConstants.END_ELEMENT)
            {
                if (XIncProcEngine.isXInclude(ev.asEndElement().getName()))
                {
                    endReadched = true;
                }
            }
        }
    }

    private void includeTextContent(final StartElement startElement, final XMLEventWriter writer)
            throws XIncludeException
    {
        final String href = startElement.getAttributeByName(new QName("href")).getValue();
        final URI source = getSource(href);

        BufferedReader brd = null;
        try
        {
            // Reading content with encoding if specified
            final StringBuilder builder = new StringBuilder();
            final URL url = source.toURL();
            final URLConnection conn = url.openConnection();
            final String contentType = conn.getContentType();
            final Attribute encodingAtt = startElement.getAttributeByName(new QName("encoding"));
            final String encoding = (encodingAtt==null)? "UTF-8" : encodingAtt.getValue();
            brd = new BufferedReader(new InputStreamReader(conn.getInputStream(), encoding));
            String line;
            while ((line = brd.readLine()) != null)
            {
                builder.append(line).append("\n");
            }
            brd.close();
            // Writing content to stat event writer
            writer.add(factory.getXmlEventFactory().createCharacters(builder.toString()));
        }
        catch (final Exception e)
        {
            throw new XIncludeException(e);
        }
        finally
        {
            Closeables.closeQuietly(brd);
            baseURIStack.pop();
        }
    }

    private void processFallback(final XMLEventReader reader, final XMLEventWriter writer)
            throws XMLStreamException, XIncludeException
    {
        final XMLEvent fallbackEvent = reader.nextTag();
        if (!fallbackEvent.isStartElement() ||
            !XIncProcEngine.isFallback(fallbackEvent.asStartElement().getName()))
        {
            throw new XIncludeException("No fallback element");
        }
        processFile(reader, writer, true);
    }

    private static void validateAttributes(final XMLEvent event) throws XIncludeException
    {
        final StartElement startElement = event.asStartElement();
        final Attribute hrefAtt = startElement.getAttributeByName(new QName("href"));
        final String href = (hrefAtt==null)? null : hrefAtt.getValue();
        final Attribute parseAtt = startElement.getAttributeByName(new QName("parse"));
        final String parse = (parseAtt==null)? "xml" : parseAtt.getValue();
        final Attribute xpointerAtt = startElement.getAttributeByName(new QName("xpointer"));
        final String xpointer = (xpointerAtt==null)? null : xpointerAtt.getValue();
        final Attribute encodingAtt = startElement.getAttributeByName(new QName("encoding"));
        final String encoding = (encodingAtt==null)? null : encodingAtt.getValue();
        final Attribute acceptAtt = startElement.getAttributeByName(new QName("accept"));
        final String accept = (acceptAtt==null)? null : acceptAtt.getValue();
        final Attribute acceptLanguageAtt = startElement.getAttributeByName(new QName("accept-language"));
        final String acceptLanguage = (acceptLanguageAtt==null)? null : acceptLanguageAtt.getValue();
        if (Strings.isNullOrEmpty(href))
        {
            if ("xml".equals(parse) && Strings.isNullOrEmpty(xpointer))
            {
                throw new XIncludeException("If the href attribute is absent when parse=\"xml\", the xpointer attribute must be present.");
            }
        }
        else
        {
            assert href != null;
            if (href.contains("#"))
            {
                throw new XIncludeException("Fragment identifiers must not be used.");
            }
        }

        if (Strings.isNullOrEmpty(parse))
        {
            if (!"xml".equals(parse) && !"text".equals(parse))
            {
                throw new XIncludeException("Parse value must be \"xml\" or \"text\".");
            }
            if ("text".equals(parse)) {
                if (!Strings.isNullOrEmpty(xpointer))
                {
                    throw new XIncludeException("The xpointer attribute must not be present when parse=\"text\"");
                }
                try
                {
                    Charset.forName(encoding);
                }
                catch (final Exception e)
                {
                    throw new XIncludeException("Encoding attribute should be a valid encoding name");
                }
            }
        }
        if (!Strings.isNullOrEmpty(accept) && XIncProcEngine.checkVal(accept))
        {
            throw new XIncludeException("Attribute \"Accept\" containing characters outside the range #x20 through #x7E");
        }
        if (!Strings.isNullOrEmpty(acceptLanguage) && XIncProcEngine.checkVal(acceptLanguage))
        {
            throw new XIncludeException("Attribute \"AcceptLanguage\" containing characters outside the range #x20 through #x7E");
        }
    }

    private void includeXmlContent(final StartElement startElement, final XMLEventWriter writer, final String xpointer)
             throws XIncludeException
    {
        final String href = startElement.getAttributeByName(new QName("href")).getValue();
        try
        {
            final URI source = getSource(href);
            final XMLEventReader readerBase = factory.getXmlInputFactory().createXMLEventReader(source.getRawPath(),
                            new FileReader(new File(source.getRawPath())));
            if (Strings.isNullOrEmpty(xpointer))
            {
                processFile(readerBase, writer, false);
            }
            else
            {
                final ByteArrayOutputStream baos = new ByteArrayOutputStream();
                final XMLEventWriter writerBase = factory.getXmlOutputFactory().createXMLEventWriter(baos);
                processFile(readerBase, writerBase, false);
                writerBase.flush();
                writerBase.close();
                parsePointer(baos.toByteArray(), writer, xpointer);
            }
        }
        catch (Exception e)
        {
            throw new XIncludeException(e);
        }
        finally
        {
            baseURIStack.pop();
        }
    }

    private void parsePointer(final byte[] bytes, final XMLEventWriter writer, final String xpointer)
                            throws XPointerException {
        try
        {
            // Getting base attribute
            String baseAtt = null;
            final InputStreamReader attReader = new InputStreamReader(new ByteArrayInputStream(bytes));
            final XMLEventReader attEvReader = factory.getXmlInputFactory().createXMLEventReader(attReader);
            while (attEvReader.hasNext())
            {
                final XMLEvent ev = attEvReader.nextEvent();
                if (ev.getEventType()==XMLStreamConstants.START_ELEMENT)
                {
                    baseAtt = ev.asStartElement().getAttributeByName(XIncProcEngine.XMLBASE_QNAME).getValue();
                    break;
                }
            }
            // Getting pointer
            final XPointer pointer = factory.getConfiguration().getXPointerEngine().parse(xpointer);
            pointer.setContext(new XPointerContext(xpointer, factory.getConfiguration()));
            final InputStreamReader in = new InputStreamReader(new ByteArrayInputStream(bytes));
            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
            final OutputStreamWriter out = new OutputStreamWriter(baos);
            // Parsing Xinclude stream with pointer
            pointer.parse(in, out);
            out.flush();
            out.close();
            // Writing parsing stream
            final InputStreamReader isr = new InputStreamReader(new ByteArrayInputStream(baos.toByteArray()));
            if (isr.ready())
            {
                final XMLEventReader r = factory.getXmlInputFactory().createXMLEventReader(isr);
                int level = 0;
                while (r.hasNext())
                {
                    final XMLEvent ev = r.nextEvent();
                    switch (ev.getEventType())
                    {
                        case XMLStreamConstants.START_DOCUMENT:
                        case XMLStreamConstants.END_DOCUMENT:
                            break;
                        case XMLStreamConstants.START_ELEMENT:
                            if (level > 0)
                            {
                                writer.add(ev);
                                if (ev.asStartElement().getAttributeByName(XIncProcEngine.XMLBASE_QNAME)==null)
                                {
                                    writeBaseAttribute(writer, baseAtt);
                                }
                            }
                            level ++;
                            break;
                        case XMLStreamConstants.END_ELEMENT:
                            if (level > 1)
                            {
                                writer.add(ev);
                            }
                            level --;                                
                            break;
                        default:
                            writer.add(ev);
                            break;
                    }
                }
            }
        }
        catch(final Exception e)
        {
            throw new XPointerException(e);
        }
    }

    private URI getSource(final String href) throws XIncludeException
    {
        try {
            URI source = new URI(href);
            baseURIStack.push(source);
            if (!source.isAbsolute()) {
                final Stack newStack = new Stack();
                newStack.addAll(baseURIStack);
                source = XIncProcEngine.getBase(newStack, (URI) newStack.pop(), baseURI).resolve(source);
            }
            return source;
        } catch (URISyntaxException e) {
            throw new XIncludeException("href must be a syntactically invalid URI or IRI.");
        }
    }

    private void writeBaseAttribute(final XMLEventWriter writer, final String baseAtt) throws XMLStreamException
    {
        if (!factory.getConfiguration().isBaseUrisFixup())
        {
            return;
        }
        Attribute attr = null;
        if (baseAtt != null)
        {
            attr = factory.getXmlEventFactory().createAttribute(XIncProcEngine.XMLBASE_QNAME, baseAtt);
        }
        else if (!baseURIStack.peek().equals(baseURI))
        {
            final URI base = baseURIStack.peek();
            attr = factory.getXmlEventFactory().createAttribute(XIncProcEngine.XMLBASE_QNAME,
                    baseURIStack.peek().toASCIIString());
        }
        if (attr != null)
        {
            writer.add(attr);
        }
    }
}
